﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Mime;
using System.Net.Security;
using System.Text.RegularExpressions;
using System.Xml.Linq;

using Ionic.Zip;

using JetBrains.Annotations;

using octalforty.Componento.Core.Framework;
using octalforty.Componento.Core.ObjectModel;
using octalforty.Componento.Core.Util;

namespace octalforty.Componento.Core.Integration.NuGet
{
    /// <summary>
    /// 
    /// </summary>
    /// <remarks>
    /// The <see cref="NuGetPackageRepositoryModule"/> can browse repositories with <c>nuget</c> schema,
    /// provide package resources with <c>nupkg</c> schema and install package resources with <c>file+nupkg</c> or <c>nupkg+file</c> schemas.
    /// </remarks>
    public class NuGetPackageRepositoryModule : IPackageRepositoryBrowser, IPackageResourceProvider,  IPackageResourceInstaller
    {
        private readonly TraceSource traceSource;
        private readonly IDictionary<string, string> cache = new Dictionary<string, string>();
        private readonly XNamespace atom = XNamespace.Get("http://www.w3.org/2005/Atom");
        private readonly XNamespace m = XNamespace.Get("http://schemas.microsoft.com/ado/2007/08/dataservices/metadata");
        private readonly XNamespace d = XNamespace.Get("http://schemas.microsoft.com/ado/2007/08/dataservices");

        public NuGetPackageRepositoryModule(TraceSource traceSource)
        {
            this.traceSource = traceSource;
        }

        #region IPackageRepositoryBrowser Members
        ReadOnlyCollection<string> IPackageRepositoryBrowser.SupportedSchemas
        {
            get { return new ReadOnlyCollection<string>(new[] { "nuget" }); }
        }

        public IEnumerable<PackageManifest> GetPackageManifests([NotNull] Uri repositoryUri, [NotNull] string name, PackageSpecification specification)
        {
            if(repositoryUri == null) throw new ArgumentNullException("repositoryUri");
            if(name == null) throw new ArgumentNullException("name");

            var packageManifests = InternalGetPackageManifests(repositoryUri, name, specification);
            Console.WriteLine("{0} manifests", packageManifests.Count());

            return packageManifests;
        }

        public PackageManifest GetPackageManifest([NotNull] Uri repositoryUri, [NotNull] string name, PackageSpecification specification)
        {
            if(repositoryUri == null) throw new ArgumentNullException("repositoryUri");
            if(name == null) throw new ArgumentNullException("name");

            var manifest = InternalGetPackageManifest(repositoryUri, name, specification);

            //
            // Since NuGet feed does not contain information on supported .NET versions,
            // we'll have to download the entire package and take a peek inside it
            if(manifest.Resources.Any())
            {
                var resourceUri = GetResourceUris(manifest, manifest.Resources[0].Uri)[0];

                var versions = new List<int>();
                var r = new Regex(@"lib[\|/]NET(?<version>\d+)/.+", RegexOptions.Compiled | RegexOptions.Singleline); // lib[\|/](?<platform>.+?)(?<version>\d+)/.+

                using(var zipFile = new ZipFile(resourceUri.LocalPath))
                {
                    var m =
                        zipFile.EntryFileNames.
                            Where(efn => r.IsMatch(efn)).
                            Select(efn => int.Parse(r.Match(efn).Groups["version"].Value)).
                            Distinct();

                    versions.AddRange(m);
                } // using

                if(versions.Any())
                {
                    foreach(var v in versions)
                    {
                        Version ver;
                        if(v < 100)
                            ver = new Version(v / 10, v % 10);
                        else
                            ver = new Version(v / 100, v / 10 % 10, v % 100);
                        manifest.AddSupportedPlatform(new PlatformVersion(Platform.NetFramework, null, ver));
                    } // foreach
                } // if
                else
                {
                    //
                    // .NET 2.0 seems to be universally supported and used
                    manifest.AddSupportedPlatform(new PlatformVersion(Platform.NetFramework, null, new Version(2, 0)));
                } // else
            } // if

            return manifest;
        }
        #endregion

        #region IPackageResourceProvider Members
        ReadOnlyCollection<string> IPackageResourceProvider.SupportedSchemas
        {
            get { return new ReadOnlyCollection<string>(new List<string>(new[] { "nupkg" })); }
        }

        public Uri[] GetResourceUris(PackageManifest package, Uri resourceUri)
        {
            var httpRequest = (HttpWebRequest)WebRequest.Create(resourceUri.ChangeScheme("http"));

            var proxy = WebRequest.GetSystemWebProxy();
            proxy.Credentials = CredentialCache.DefaultCredentials;
            
            httpRequest.Method = "GET";
            httpRequest.Proxy = proxy;
            httpRequest.PreAuthenticate = true;
            httpRequest.AuthenticationLevel = AuthenticationLevel.MutualAuthRequested;

            using(var httpResponse = (HttpWebResponse)httpRequest.GetResponse())
            {
                if(httpResponse.StatusCode == HttpStatusCode.OK)
                {
                    string name;
                    if(!string.IsNullOrEmpty(httpResponse.Headers["Content-Disposition"]))
                    {
                        var contentDisposition = new ContentDisposition(httpResponse.Headers["Content-Disposition"]);
                        name = contentDisposition.FileName;
                    } // if
                    else
                        name = resourceUri.GetLastSegment();

                    var temp = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
                    if(!Directory.Exists(temp))
                        Directory.CreateDirectory(temp);

                    
                    var n = 2048;
                    var buffer = new byte[n];
                    var fullpath = Path.Combine(temp, name);

                    using(var responseStream = httpResponse.GetResponseStream())
                    {
                        
                        using(var f = File.Open(fullpath, FileMode.Create, FileAccess.Write))
                        {
                            int r;
                            while((r = responseStream.Read(buffer, 0, n)) != 0)
                                f.Write(buffer, 0, r);
                        } // using
                    } // using

                    return new[] { new UriBuilder(new Uri(fullpath)) { Scheme = "file+nupkg" }.Uri };
                } // if
                else
                    throw new Exception();
                
            } // using
        }
        #endregion

        #region IPackageResourceInstaller Members
        ReadOnlyCollection<string> IPackageResourceInstaller.SupportedSchemas
        {
            get { return new ReadOnlyCollection<string>(new List<string>(new[] { "file+nupkg", "nupkg+file" }));}
        }

        public void InstallResource(Uri resourceUri, string targetDirectory)
        {
            using(var zipFile = new ZipFile(resourceUri.LocalPath))
            {
                foreach(var zipEntry in zipFile.Entries.Where(e => e.FileName.ToLowerInvariant().StartsWith("lib")))
                {
                    var destinationBase = Path.Combine(targetDirectory, "bin");

                    var destination = Path.Combine(destinationBase, zipEntry.FileName.SubstringAfter(@"lib").TrimStart('\\', '/'));
                    if(!Directory.Exists(Path.GetDirectoryName(destination)))
                        Directory.CreateDirectory(Path.GetDirectoryName(destination));

                    using(var fileStream = File.Open(destination, FileMode.Create, FileAccess.Write))
                        zipEntry.Extract(fileStream);

                    /*foreach(var mappingTarget in pathMapping)
                    {
                        var sub = pathMapping[mappingTarget].Substring(2);

                        var zipFileName = zipEntry.FileName.ToLowerInvariant();
                        if(!zipEntry.IsDirectory && (sub.Length == 0 || (zipFileName.StartsWith(sub.ToLowerInvariant()) && zipFileName.Length > sub.Length)))
                        {
                            string dest = PathBuilder.BuildPath(destinationPathRoot, mappingTarget);
                            string sfp = zipEntry.FileName.Substring((sub.Length == 0 ? -1 : sub.Length) + 1);


                            string fullPath = Path.Combine(dest, sfp);

                            string dir = Path.GetDirectoryName(fullPath);

                            if(!Directory.Exists(dir))
                                Directory.CreateDirectory(dir);

                            using(FileStream fs = File.Open(fullPath, FileMode.Create))
                                zipEntry.Extract(fs);
                        } // if
                    } // foreach
                     * */
                } // foreach
            } // using
        }
        #endregion

        private PackageManifest InternalGetPackageManifest(Uri repositoryUri, string name, PackageSpecification specification)
        {
            var uri = BuildFilterUri(repositoryUri.ChangeScheme("http").EnsureEndsWith("/").EnsureEndsWith("Packages"), name, specification);
            var xdocument = LoadXDocument(uri.ToString());

            var packages = xdocument.Element(atom + "feed").Elements(atom + "entry");
            var package = packages.FirstOrDefault();

            return package == null ? 
                new PackageManifest("!" + name, 
                    specification == null || specification.Version == null || specification.Version.Version == null ? 
                        new Version(0, 0) : 
                        specification.Version.Version, "") : 
                ConvertAtomEntryToPackageManifest(package);
        }

        private XDocument LoadXDocument(string uri)
        {
            if(!cache.ContainsKey(uri))
            {
                cache[uri] = XDocument.Load(uri).ToString(SaveOptions.DisableFormatting);
            } // if

            return XDocument.Parse(cache[uri]);
        }

        private IEnumerable<PackageManifest> InternalGetPackageManifests(Uri repositoryUri, string name, PackageSpecification specification)
        {
            var uri = BuildFilterUri(repositoryUri.ChangeScheme("http").EnsureEndsWith("/").EnsureEndsWith("Packages"), name, specification);
            var xdocument = LoadXDocument(uri.ToString());

            var packages = xdocument.Element(atom + "feed").Elements(atom + "entry");

            return packages.Select(p => ConvertAtomEntryToPackageManifest(p));
        }

        private PackageManifest ConvertAtomEntryToPackageManifest(XElement entry)
        {
            var resourceUri = new UriBuilder(entry.Element(atom + "content").Attribute("src").Value) { Scheme = "nupkg" }.Uri;

            //
            // Dependencies are stored in a fucked-up way: Argotic.Common:2008.0.2.0|Argotic.Extensions:2008.0.2.0
            var deps = entry.Element(m + "properties").Element(d + "Dependencies").Value;
            var dependencies = string.IsNullOrEmpty(deps) ?
                null :
                ParseDependencies(deps);

            var projectUri = entry.Element(m + "properties").Element(d + "ProjectUrl").Value;

            var manifest = new PackageManifest(
                entry.Element(m + "properties").Element(d + "Id").Value.ToLowerInvariant(),
                new Version(entry.Element(m + "properties").Element(d + "Version").Value),
                entry.Element(m + "properties").Element(d + "Summary").Value,
                string.IsNullOrEmpty(projectUri) ? null : new Uri(projectUri),
                dependencies,
                new[] { new PackageResource(resourceUri, long.Parse(entry.Element(m + "properties").Element(d + "PackageSize").Value))  }, null);/*
                new Uri(entry.Element(atom + "id").Value),
                    null, null), 
                    new[] { new Resource(resourceUri) }, 
                    new[] { 
                        new PathMapping(new CliEnvironment(CliType.NetFramework, new Version(2, 0)), IsaType.Cil) {
                            { PathMappingTarget.Binaries, "~/lib/net20" },
                            { PathMappingTarget.Binaries, "~/lib/net35" }
                        },
                        new PathMapping(new CliEnvironment(CliType.NetFramework, new Version(3, 5)), IsaType.Cil) {
                            { PathMappingTarget.Binaries, "~/lib/net20" },
                            { PathMappingTarget.Binaries, "~/lib/net35" }
                        }
                    }, 
                    dependencies);*/

            return manifest;
            
        }

        private IEnumerable<PackageReference> ParseDependencies(string deps)
        {
            try
            {
                var d = deps.Split('|').Select(d2 => d2.Split(':')).ToList();
                return d.Select(dep => new PackageReference(dep[0].ToLowerInvariant(), VersionRequirement.Parse(dep[1])));

            }
            catch(Exception e)
            {
                throw;
            }
            
        }

        private static Uri BuildFilterUri(Uri baseUri, string name, PackageSpecification specification)
        {
            var filterUri = baseUri;
            var args = new Dictionary<string, string>();
            if(name.Contains("*"))
            {
                var effectiveName = name.Trim('*');
                var filter = 
                    name.IsSurroundedWith("*") ?
                        "substringof('{0}',Id)".FormatWith(effectiveName) :
                    name.StartsWith("*") ?
                        "endswith(Id,'{0}')".FormatWith(effectiveName) :
                    name.EndsWith("*") ?
                        "startswith(Id,'{0}')".FormatWith(effectiveName) :
                        "startswith(Id,'{0}') and endswith(Id,'{1}')".FormatWith(effectiveName.SubstringBefore("*"), effectiveName.SubstringAfter("*"));

                args["$filter"] = filter;
            } // if
            else
                args["$filter"] = "Id eq '{0}'".FormatWith(name);
            
            if(specification != null && specification.Version != null && specification.Version.Version != null)
            {
                //
                // By convention, .NET can load assemblies with Version.Revision being different from
                // one originally linked against. Handle this case here.
                if(specification.Version.Version.Build != -1)
                    args["$filter"] = "({0}) and (startswith(Version,'{1}.') or Version eq '{1}')".FormatWith(args["$filter"], specification.Version.Version.ToString(3));
                else
                    args["$filter"] = "({0}) and (Version eq '{1}')".FormatWith(args["$filter"], specification.Version.Version);
            } // if

            if(args.Count > 0)
            {
                filterUri = baseUri.EnsureEndsWith("()");

                foreach(var arg in args)
                    filterUri = filterUri.AppendQueryString(arg.Key, arg.Value);
            } // if

            return filterUri;
        }
    }
}
